第 4 章:組態檔語法與撰寫原則
原始類型
string
代表 Unicode 字串,例如:"hello"
number
代表數字:整數、小數bool
代表布林值:true
、false
number
和bool
都可以和string
進行隱式轉換,當我們把number
或bool
類型的值賦給string
類型的值,或是反過來時,Terraform 會自動替我們轉換類型,其中:
true
⭤"true"
false
⭤"false"
15
⭤"15"
3.1415
⭤"3.1415"
複雜類型
集合類型 list、map、set
一個集合包含了一組同一類型的值。集合內元素的類型成為元素類型。一個集合變數在構造時必須確定集合類型。集合內所有元素的類型必須相同。
list(...)
列表是一組值的連續集合,可以用下標存取內部元素,下標從 0 開始。例如名為 l 的 list,l[0] 就是第一個元素。list 類型的宣告可以是 list(number)、list(string)、list(bool) 等,括號中的類型即為元素類型。
map(...)
字典類型(映射類型),代表一組鍵唯一的鍵值對,鍵類型必須是 string
,值類型任意。map(number)
代表鍵為 string
類型而值為 number
類型,其餘類推。
map
值有兩種宣告方式
-
{ "foo": "bar", "bar": "baz" }
-
{ foo="bar", bar="baz" }
-
鍵可以不用雙引號,但如果鍵是以數字開頭則例外
-
多對鍵值對之間要用逗號分隔,也可以用換行符號分隔。建議使用
=
號 -
set(...)
集合類型,代表一組不重複的值。
以上集合類型都支援通配類型縮寫,例如 list
等價於 list(any)
,map
等價於 map(any)
,set
等價於 set(any)
。any
代表支援任意的元素類型,前提是所有元素都是一個類型。例如,將 list(number)
賦給 list(any)
是合法的,list(string)
賦給 list(any)
也是合法的,但是 list
內部所有的元素必須是同一種類型的。
結構化類型 object、tuple
一個結構化類型允許多個不同類型的值組成一個類型。結構化類型需要提供一個 schema
結構資訊作為參數來指明元素的結構。
-
object(...)
- 物件是指一組由具有名稱和類型的屬性所構成的符合類型
- schema
{ \<KEY\>=\<TYPE\>, \<KEY\>=\<TYPE\>,...}
object({age=number, name=string})
- 代表由名為
"age"
類型為number
,以及名為"name"
類型為"string"
兩個屬性組成的物件。賦給 - 例如對
object({age=number,name=string})
{ age=18 }
是一個非法值{ age=18, name="john", gender="male" }
是一個合法值,但賦值時gender
會被拋棄
-
tuple(...)
- 元組類似也是一組值的連續集合,但每個元素都有獨立的類型。
- schema:
[\<TYPE\>, \<TYPE\>, ...]
- 元組的元素數量必須 與 schema 宣告的型別數量相等,且每個元素的型別必須與元組 schema 對應位置的型別相等。例如
tuple([string, number, bool])
,類型的一個合法值可以是["a", 15, true]
複雜型別也支援隱式型別轉換。
Terraform 會嘗試轉換類似的類型,轉換規則有:
object
和map
:如果一個map
的鍵集合含有object
規定的所有屬性,那麼map
可以被轉換為object
,map
裡多餘的鍵值對會被拋棄。由map
→object
→map
的轉換可能會遺失資料。tuple
和list
:當一個list
元素的數量剛好等於一個tuple
宣告的長度時,list
可以轉換為tuple
。例如:值為["18", "true", "john"]
的list
轉換為tuple([number,bool, string])
的結果為[18, true, "john"]
set
和tuple
:當一個list
或tuple
被轉換為一個set
,那麼重複的值將被丟棄,並且值原有的順序也會遺失。如果一個set
被轉換到list
或是tuple
,那麼元素將按照以下順序排列:如果set
的元素是string
,那麼將按照字段順序排列;其他類型的元素不承諾任何特定的排列順序。複雜型別轉換時,元素類型將在可能的情況下發生隱式轉換,類似上述list
到tuple
轉換舉的例子。- 如果類型不匹配,Terraform 會報錯,例如我們試圖把
object({name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12})
轉換到map(string)
類型,這是不合法的,因為name
的值為list
,無法轉換為string
。
佔位符 any
any
是 Terraform 中非常特殊的一種類型約束,它本身並非一個類型,而只是一個佔位符。每當一個值被賦予一個由 any
約束的複雜類型時,Terraform 會嘗試計算出一個最精確的類型來取代 any
。
例如我們把 ["a", "b", "c"]
賦給 list(any)
,它在 Terraform 中實際的物理類型首先被編譯成 tuple([string, string, string])
,然後 Terraform 認為 tuple
和 list
相似,所以會嘗試將它轉換為 list(string)
。然後 Terraform 發現 list(string)
符合 list(any)
的約束,所以會用 string
取代 any
,於是賦值後最終的型別是 list(string)
。
由於即使是 list(any)
,所有元素的類型也必須是一樣的,所以某些類型轉換到 list(any)
時會對元素進行隱式類型轉換。例如將 ["a", 1, "b"]
賦給 list(any)
,Terraform 發現 1
可以轉換到 "1"
,所以最終的值是 ["a", "1", "b"]
,最終的型別會是 list(string)
。再例如我們想把 ["a", \[\], "b"]
轉換成 list(any)
,由於 Terraform 無法找到一個合適的目標類型使得所有元素都能成功隱式轉換過去,所以 Terraform 會報錯,要求所有元素都必須是同一個類型的。
宣告類型時如果不想有任何的約束,那麼可以用 any
variable "no_type_constraint" {
type = any
}
無類型 null
代表資料缺失。如果我們把一個參數設為 null
,Terraform 會認為你忘記為它賦值。如果該參數有預設值,那麼 Terraform 會使用預設值;如果沒有又剛好該參數是必填字短,Terraform 會報錯。null
在條件式中非常有用,你可以在某項條件不滿足時跳過對某參數的賦值。
object 的 optional 成員
如果一個 variable
的類型為 object
,那麼使用時必須傳入一個結構完全相符的物件。
variable "an_object" {
type = object({
a = string
b = string
c = number
})
}
如果我們想要傳入一個物件給 var.an_object
,但不準備給 b
和 c
賦值,我們必須這樣:
{
a = "a"
b = null
c = null
}
傳入的物件必須完全符合類型定義的結構,即使我們不想對某些屬性賦值。這使得我們如果想要定義一些比較複雜,屬性比較多的 object
類型時會給使用者在使用上造成一些麻煩。
with_optional_attribute
variable "with_optional_attribute" {
type = object({
a = string # a required attribute
b = optional(string) # an optional attribute
c = optional(number, 127) # an optional attribute with default value
})
}
這裡我們將 b
宣告為 optional
,如果傳入的物件沒有 b
,則會使用 null
作為值;c
不但宣告為 optional
的,還添加了 127
作為預設值,傳入的物件如 果沒有 c
,那麼會使用 127
作為它的值。
optional
修飾符有這樣兩個參數:
- 類型:(必填)第一個參數標明了屬性的類型
- 預設值:(選填)第二個參數定義了 Terraform 在物件中沒有定義該屬性值時所使用的預設值。預設值必須與類型參數相容。如果沒有指定預設值,Terraform 會使用
null
作為預設值。一個包含非null
預設值的optional
屬性在模組內使用時可以確保不會讀到null
值。當使用者沒有設定該屬性,或是明確設定為null
時,Terraform 會使用預設值,所以模組內無需再次判斷該屬性是否為null
。
Terraform 採用自上而下的順序來設定物件的預設值,也就是說,Terraform 會先套用 optional
修飾符中的指定的預設值,然後再為其中可能存在的內嵌物件設定預設值。
帶有 optional 屬性和預設值的內嵌結構
下面的範例示範了一個輸入變數,用來描述一個儲存了靜態網站內容的儲存桶。此變數的型別包含了一系列的 optional
屬性,包括 website
,不但其本身是 optional
的,其內部包含了數個 optional
的屬性以及預設值。
optional embedding
variable "buckets" {
type = list(object({
name = string
enabled = optional(bool, true)
website = optional(object({
index_document = optional(string, "index.html")
error_document = optional(string, "error.html")
routing_rules = optional(string)
}), {})
}))
}
以下給出一個範例 terraform.tfvars
文件,為 var.buckets
定義了三個儲存桶:
production
配置了一條重定向的路由規則archived
使用了預設配置,但被關閉了docs
使用文字檔案取代了索引頁和錯誤頁production
桶子沒有指定索引頁和錯誤頁,archived
桶子完全忽略了網站配置。Terraform 會使用bucket
類型約束中指定的預設值。
terraform.tfvars
buckets = [
{
name = "production"
website = {
routing_rules = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]
EOT
}
},
{
name = "archived"
enabled = false
},
{
name = "docs"
website = {
index_document = "index.txt"
error_document = "error.txt"
}
},
]
此配置會產生如下的 variable
值:
- 對
production
和docs
桶,Terraform 會將enabled
設為true
。Terraform 會同時使用預設值配置website
,然後使用docs
中指定的值來覆寫預設值。 - 對
archived
和docs
桶,Terraform 會將routing_rules
設為null
。當 Terraform 沒有讀取到optional
的屬性,且屬性上沒有設定預設值時,Terraform 會將這些屬性設為null
。 - 對於
archived
桶,Terraform 會將website
屬性設為buckets
類型約束中定義的預設值。
variable
tolist([
{
"enabled" = true
"name" = "production"
"website" = {
"error_document" = "error.html"
"index_document" = "index.html"
"routing_rules" = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]
EOT
}
},
{
"enabled" = false
"name" = "archived"
"website" = {
"error_document" = "error.html"
"index_document" = "index.html"
"routing_rules" = tostring(null)
}
},
{
"enabled" = true
"name" = "docs"
"website" = {
"error_document" = "error.txt"
"index_document" = "index.txt"
"routing_rules" = tostring(null)
}
},
])
範例:有條件地設定一個預設屬性
有時我們需要根據其他資料的值來動態決定是否要為一個 optional
參數設定值。在這種場景下,發起呼叫的 module
區塊可以使用條件表達式搭配 null
來動態地決定是否設定該參數。
還是上一個例子中的 variable "buckets"
的例子,使用下面演示的例子可以根據新輸入參數 var.legacy_filenames
的值來有條件地覆蓋 website
對象中 index_document
以及 error_document
的設定:
define default value
variable "legacy_filenames" {
type = bool
default = false
nullable = false
}
module "buckets" {
source = "./modules/buckets"
buckets = [
{
name = "maybe_legacy"
website = {
error_document = var.legacy_filenames ? "ERROR.HTM" : null
index_document = var.legacy_filenames ? "INDEX.HTM" : null
}
},
]
}
當 var.legacy_filenames
設定為 true
時,呼叫會覆蓋 document
的檔名。當它的值為 false
時,呼叫不會指定這兩個檔名,這使得模組使用定義的預設值。
配置語法
參數
image_id = "abc123"
參數賦值就是將一個值賦給一個特定的名稱。參數值可以是確定的字面量硬編碼,也可以是一組表達式,用以透過其他的值加以計算結果值。
區塊
resource "aws_instance" "example" {
ami = "abc123"
network_interface {
# ...
}
}
一個區塊是包含一組其他內容的容器,一個區塊有一個類型(上面的例子裡類型就是 resource)。每個區塊類型都定義了類型關鍵字後面要跟多少標籤,例如 resource 區塊規定了後面要跟兩個標籤-在例子裡就是 aws_instance
和 example
。一個區塊類型可以規定任意多個標籤,也可以沒有標籤,例如內嵌的 network_interface
區塊。
在塊類型及其後續標籤之後,就是塊體。塊體必須被包含在一對花括號中間。在區塊體中可以進一步定義各種參數和其他的區塊。
Terraform 規範定義了有限多個頂級區塊類型,也就是可以遊離任何其他區塊獨立定義在設定檔中的區塊。大部分的 Terraform 功能(例如 resource, variable, output, data 等)都是頂級區塊。
標識符
參數名稱、區塊類型名稱以及其他 Terraform 規格中定義的結構的名稱,例如 resource、variable 等,都是識別碼。
合法的識別碼可以包含字母、數字、底線 (_) 以及減號 (-)。標識 符首字母不可以為數字。
若要了解完整的識別碼規範,請造訪 Unicode 標識符語法。
註釋
#
單行註釋,其後的內容為註釋//
單行註釋,其後的內容為註釋/*
和/
,多行註釋,可以註解多行預設情況下單行註解優先使用 #。自動化格式整理工具會自動把 // 換成 #。
編碼以及換行
Terraform 設定檔必須始終使用 UTF-8 編碼。分隔符號必須使用 ASCII 符號,其他識別碼、註解以及字串字面量均可使用非 ASCII 字元。
Terraform 相容於 Unix 風格的換行符以及 Windows 風格的換行符,但理想狀態下應使用 Unix 風格換行符。
輸入變數
如果我們想要在建立、修改基礎設施時動態傳入一些值呢?比如說在程式碼中定義 Provider 時用變數取代硬編碼的存取密鑰,或是由建立基礎架構的使用者來決定建立什麼樣尺寸的主機?我們需要的是輸入變數。
如果我們把一組 Terraform 程式碼想像成一個函數,那麼輸入變數就是函數的入參。輸入變數以 variable 區塊進行定義。
input variable
variable "image_id" {
type = string
}
variable "availability_zone_names" {
type = list(string)
default = ["us-west-1a"]
}
variable "docker_ports" {
type = list(object({
internal = number
external = number
protocol = string
}))
default = [
{
internal = 8300
external = 8300
protocol = "tcp"
}
]
}
這些都是合法的輸入參數定義。緊跟 variable
關鍵字的就是變數名。在一個 Terraform 模組(同一個資料夾中的所有 Terraform 程式碼文件,不包含子資料夾)中變數名稱必須是唯一的。我們在程式碼中可以透過 var.<NAME>
的方式引用變數的值。有一組關鍵字不可以被用來當作輸入變數的名字:
- source
- version
- providers
- count
- for_each
- lifecycle
- depends_on
- locals
輸入變數只能在宣告該變數的目錄下的程式碼中使用。
輸入變數區塊中可以定義一些屬性。
類型 type
可以在輸入變數區塊中透過 type
定義類型,例如:
variable "name" {
type = string
}
variable "ports" {
type = list(number)
}
定義了類型的輸入變數只能被賦予符合類型約束的值。
預設值 default
預設值定義了當 Terraform 無法獲得一個輸入變數得到值的時候會使用的預設值。例如:
variable "name" {
type = string
default = "John Doe"
}
當 Terraform 無法透過其他途徑獲得 name 的值時,var.name
的值為 "John Doe"
。
描述 description
可以在輸入變數中定義一個描述,簡單地向呼叫者描述該變數的意義和用法:
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
}
如果在執行 terraform plan
或是 terraform apply
時 Terraform 不知道某個輸入變數的值,Terraform 會在命令列介面上提示我們為輸入變數設定一個值。例如上面的輸入變數程式碼,執行 terraform apply
時:
terraform apply
var.image_id
The id of the machine image (AMI) to use for the server.
Enter a value:
為了使的程式碼的使用者能夠準確地理解輸入變數的意義和用法,我們應該站在使用者而非程式碼維護者的角度編寫輸入變數的描述。描述並不是註解!
斷言 assert
確保輸入參數的類型是正確的
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
validation {
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}
condition 參數是一個 bool 類型的參數,我們可以用一個表達式來定義如何界定輸入變數是合法的。
當 contidion 為 true 時 輸入變數合法,反之不合法。condition 表達式中只能透過 var.\ 引用目前定義的變量,並且它的計算不能產生錯誤。
假如表達式的計算產生一個錯誤是輸入變數驗證的一種判定手段,那麼可以使用 can 函數來判定表達式的執行是否拋錯。例如:
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
validation {
# regex(...) fails if it cannot find a match
condition = can(regex("^ami-", var.image_id))
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}
在上述例子中,如果輸入的 image_id
不符合正規表示式的要求,那麼 regex
函數呼叫就會拋出一個錯誤,這個錯誤就會被 can
函數捕獲,輸出 false
。
condition
表達式如果為 false
,Terraform 會傳 error_message
回中定義的錯誤訊息。error_message
應該完整描述輸入變數校驗失敗的原因,以及輸入變數的合法約束條件。
在命令列輸出中隱藏值
將變數設為 sensitive
可以防止我們在設定檔中使用變數時 Terraform 在 plan
和 apply
指令的輸出中展示與變數相關的值。
Terraform 仍然會將敏感資料記錄在狀態檔案中,任何可以存取狀態檔案的人都可以讀取到明文的敏感資料值。
宣告一個變數包含敏感資料值需要將 sensitive
參數設定為 true
:
variable "user_information" {
type = object({
name = string
address = string
})
sensitive = true
}
resource "some_resource" "a" {
name = var.user_information.name
address = var.user_information.address
}
任何使用了敏感變數的表達式都將被視為敏感的,因此在上面的範例中, resource “some_resource” “a”
的兩個參數也將在計劃輸出中被隱藏:
Terraform will perform the following actions:
# some_resource.a will be created
+ resource "some_resource" "a" {
+ name = (sensitive)
+ address = (sensitive)
}
Plan: 1 to add, 0 to change, 0 to destroy.
在某些情況下,我們會在巢狀區塊中使用敏感變量,Terraform 可能會將整個區塊視為敏感的。這發生在那些包含有要求值是唯一的內嵌區塊的資源中,公開這種內嵌區塊的部分內容可能會暗示兄弟區塊的內容。
# some_resource.a will be updated in-place
~ resource "some_resource" "a" {
~ nested_block {
# At least one attribute in this block is (or was) sensitive,
# so its contents will not be displayed.
}
}
Provider 還可以將資源屬性宣告為敏感屬性,這將導致 Terraform 將其從常規輸出中隱藏。
如果打算使用敏感值作為輸出值的一部分,Terraform 會要求您將輸出值本身標記為敏感值,以確認確實打算將其匯出。
Terraform 可能暴露敏感變數的情況
sensitive
變數是一個以設定檔為中心的概念,值會毫無混淆地傳送給 Provider。如果該值包含在錯誤訊息中,則 Provider 報錯時可能會暴露該值。例如,即使「foo」 是敏感值,Provider 也可能傳回下列錯誤:"Invalid value 'foo' for field"
如果將資源屬性用作、或是作為 Provider 定義的資源 ID 的一部分,則 apply
將公開該值。在下面的範例中,前綴屬性已設定為 sensitive
變量,但隨後該值(“jae”)作為資源ID 的一部分公開:
sensitive
# random_pet.animal will be created
+ resource "random_pet" "animal" {
+ id = (known after apply)
+ length = 2
+ prefix = (sensitive)
+ separator = "-"
}
Plan: 1 to add, 0 to change, 0 to destroy.
...
random_pet.animal: Creating...
random_pet.animal: Creation complete after 0s [id=jae-known-mongoose]
禁止輸入變數為空
輸入變數的 nullable
參數控制模組呼叫者是否可以將 null 指派給變數。
variable "example" {
type = string
nullable = false
}
nullable
的預設值為 true
。當 nullable
為 true
時,null
是變數的有效值,且模組程式碼必須始終考慮變數值為 null
的可能性。將 null
作為模組輸入參數傳遞將覆蓋輸入變數上定義的預設值。
將 nullable
設為 false
可確保變數值在模組內永遠不會為空。如果 nullable
為 false
且輸入變數定義有預設值,則當模組輸入參數為 null
時,Terraform 將使用預設值。
nullable
參數僅控制變數的直接值可能為 null
的情況。對於集合或物件類型的變量,例如列表或對象,呼叫者仍然可以在集合元素或屬性中使用null,只要集合或物件本身不為 null
。
對輸入變數賦值
命令列參數
對輸入變數賦值有幾種途徑,一種是在呼叫 terraform plan
或是 terraform apply
指令時以參數的形式傳入:
terraform apply -var="image_id=ami-abc123"
terraform apply -var='image_id_list=["ami-abc123","ami-def456"]'
terraform plan -var='image_id_map={"us-east-1":"ami-abc123","us-east-2":"ami-def456"}'
可以在一條指令中使用多個 -var
參數。
參數文件
第二種方法是使用參數檔。參數檔的後綴名可以是 .tfvars
或是 .tfvars.json
。.tfvars
檔案使用 HCL 語法,.tfvars.json
使用 JSON 語法。
以 .tfvars
為例,參數檔中以 HCL 程式碼對需要賦值的參數進行賦值,例如:
image_id = "ami-abc123"
availability_zone_names = [
"us-east-1a",
"us-west-1c",
]
後綴名為 .tfvars.json
的檔案用一個 JSON 物件來對輸入變數賦值,例如:
{
"image_id": "ami-abc123",
"availability_zone_names": ["us-west-1a", "us-west-1c"]
}
呼叫 terraform 指令時,透過 -var-file
參數指定要使用的參數文件,例如:
terraform apply -var-file="testing.tfvars"
terraform apply -var-file="testing.tfvars.json"
有兩種情況,你無需指定參數檔:
- 目前模組內有名為
terraform.tfvars
或是terraform.tfvars.json
的文件 - 目前模組內有一個或多個後綴名為
.auto.tfvars
或是.auto.tfvars.json
的文件Terraform 會自動使用這兩種自動參數檔對輸入參數賦值。
環境變量
可以透過設定名為 TF_VAR_<NAME>
的環境變數為輸入變數賦值,例如:
export TF_VAR_image_id=ami-abc123
terraform plan
...
在環境變數名稱大小寫敏感的作業系統上,Terraform 要求環境變數中的 \ 與 Terraform 程式碼中定義的輸入變數名稱大小寫完全一致。
環境變數傳值非常適合在自動化管線中使用,尤其適合用來傳遞敏感數據,類似密碼、存取金鑰等。
互動介面傳值
在前面介紹斷言的例子中我們看到過,當我們從命令列介面執行 terraform 操作,Terraform 無法透過其他途徑取得一個輸入變數的值,而該變數也沒有定義預設值時,Terraform 會進行最後的嘗試,在互動介面上要求我們給出變數值。
輸入變數賦值優先權
當上述的賦值方式同時存在時,同一個變數可能會被賦值多次。Terraform 會使用新值覆蓋舊值。
Terraform 載入變數值的順序是:
- 環境變數
terraform.tfvars
文件(如果存在的話)terraform.tfvars.json
文件(如果存在的話)- 所有的
.auto.tfvars
或.auto.tfvars.json
文件,以字母順序排序處理 - 透過
var
或是var-file
命令列參數傳遞的輸入變量,按照命令列參數中定義的順序加載
假如以上方式皆未能成功對變數賦值,那麼 Terraform 會嘗試使用預設值;對於沒有定義預設值的變量,Terraform 會採用互動介面方式要求使用者輸入一個。對於某些 Terraform 指令,如果執行時帶有 -input=false
參數停用了互動 介面傳值方式,那麼就會報錯。
複雜型別傳值
透過參數檔傳值時,可以直接使用 HCL 或 JSON 語法對複雜型別傳值,例如 list 或 map。
對於某些場景下必須使用 -var
命令列參數,或是環境變數傳值時,可以用單引號引用 HCL 語法的字面量來定義複雜類型,例如:
export TF_VAR_availability_zone_names='["us-west-1b","us-west-1d"]'
由於採用這種方法需要手動處理引號的轉義,所以這種方法比較容易出錯,複雜類型的傳值建議盡量通過參數檔。
輸出值
我們在介紹輸入變數時提到過,如果我們把一組 Terraform 程式碼想像成一個函數,那麼輸入變數就是函數的入參;函數可以有入參,也可以有回傳值,同樣的,Terraform 程式碼也可以有回傳值,這就是輸出值。
大部分語言的函數只支援無回傳值或是單回傳值,但是 Terraform 支援多回傳值。在目前模組 apply 一段 Terraform 程式碼,執行成功後命令列會輸出程式碼中定義的回傳值。另外我們也可以透過 terraform output
指令來輸出目前模組對應的狀態檔中的回傳值。
輸出值的聲明
輸出值的聲明使用輸出區塊,例如:
output "instance_ip_addr" {
value = aws_instance.server.private_ip
}
output
關鍵字後面緊接的就是輸出值的名稱。在目前模組內的所有輸出值的名字都必須是唯一的。output
區塊內的 value
參數即為輸出值,它可以像上面的範例裡那樣某個 resource 的輸出屬性,也可以是任意合法的表達式。
輸出值只有在執行 terraform apply
後才會被計算,光是使用 terraform plan
並不會計算輸出值。
Terraform 程式碼中無法引用本目錄下定義的輸出值。
output
區塊還有一些可選的屬性:
description
output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."
}
sensitive
一個輸出值可以標記 sensitive
為 true
,表示該輸出值含有敏感資訊。被標記 sensitive 的輸出值只是在執行 terraform apply
指令成功後會印出""以取代真實的輸出值,執行 terraform output
時也會輸出 "",但仍可以透過執行 terraform output -json
看到實際的敏感值。
需要注意的是,標記為 sensitive 輸出值仍然會被記錄在狀態檔案中,任何有權限讀取狀態檔案的人仍然可以讀取到敏感資料。
depends_on
關於 depends_on
的內容將在 resource 章節中詳細介紹,所以這裡我們只是粗略地介紹一下。
Terraform 會解析程式碼所定義的各種 data、resource,以及他們之間的依賴關係,例如,建立虛擬機器時用的 image_id
參數是透過 data 查詢而來的,那麼虛擬機器實例就依賴這個鏡像的 data, Terraform 會先建立 data,得到查詢結果後,再建立虛擬機器 resource。一般來說,data、resource 之間的建立順序是由 Terraform 自動計算的,不需要程式碼的編寫者明確指定。但有時有些依 賴關係無法透過分析程式碼得出,這時我們可以在程式碼中透過 depends_on
明確聲明依賴關係。
一般 output 很少會需要明確依賴某些資源,但有一些特殊場景,例如在當前程式碼中呼叫一個模組(可以理解成調用另一個目錄中的 Terraform 程式碼創建一些資源)時,呼叫者希望在模組資源全部創建完畢以後才繼續後續的創建工作,這時我們可以為模組設計一個 output,透過 depends_on
明確聲明依賴關係,以確保該 output 必須在所有模組資源成功創建以後才能被讀取,這樣我們就可以在模組尺度上控制資源的建立順序。
output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."
depends_on = [
# Security group rule must be created before this IP address could
# actually be used, otherwise the services will be unreachable.
aws_security_group_rule.local_access,
]
}
我們不鼓勵針對 output 定義 depends_on
,只能作為最後的手段加以應用。如果不得不針對 output 定義 depends_on
,請務必透過註解說明原因,方便後人進行維護。
precondition
output
區塊從 Terraform v1.2.0 開始也可以包含一個 precondition
區塊。
output
塊上的 precondition
對應於 variable
塊中的 validation
塊。validation
區塊檢查輸入變數值是否符合模組的要求,precondition
確保模組的輸出值符合某種要求。我們可以透過 precondition
來防止 Terraform 把一個不合法的處置值寫入狀態檔。我們可以在適當的場景下通過 precondition
來保護上一次 apply
留下的合法的輸出值。
Terraform 在計算輸出值的 value
表達式之前執行 precondition
檢查,這可以防止 value
表達式中的潛在錯誤被激發。
局部值
有時我們會需要用一個比較複雜的表達式計算某一個值,並且重複使用之,這時我們把這個複雜表達式賦予一個局部值,然後反覆引用該局部值。如果說輸入變數相當於函數的入參,輸出值就相當於函數的回傳值,那麼局部值就相當於函數內定義的局部變數。
局部值透過 locals
區塊定義,例如:
locals {
service_name = "forum"
owner = "Community Team"
}
一個 locals
區塊可以定義多個局部值,也可以定義任意多個 locals
區塊。賦給局部值的可以是更複雜的表達式,也可以是其他 data、resource 的輸出、輸入變量,甚至是其他的局部值:
locals {
# Ids for multiple sets of EC2 instances, merged together
instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}
locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}
引用局部值的表達式是 local.<NAME>
(注意,雖然局部值定義在 locals
區塊內,但引用是務必使用 local
而不是 locals
),例如:
resource "aws_instance" "example" {
# ...
tags = local.common_tags
}
局部值只能在同一模組內的程式碼中引用。
局部值可以幫助我們避免重複複雜的表達式,提升程式碼的可讀性,但如果過度使用也有可能增加程式碼的複雜度,使得程式碼的維護者更難理解所使用的表達式和值。適度使用局部值,僅用於重複引用相同複雜表達式的場景,未來當我們需要修改該表達式時局部值將使得修改變得相當輕鬆。
重載文件
一般來說 Terraform 會載入模組內所有的 .tf
和 .tf.json
文件,並要求文件內定義了一組無重複的物件。如果兩個檔案嘗試定義同一個對象,那麼 Terraform 會報錯。
在某些少見場景中,能夠用單獨的檔案重載已有物件配置的特定部分將會十分有用。比如說,由工程師編寫的設定檔能夠在執行時被程式產生的 JSON 檔案部分重載。
為支援這些少見場景,Terrform 會對後綴名為 override.tf
和 override.tf.json
的程式碼檔案進行特殊處理。對於名為 override.tf
和 override.tf.json
的程式碼檔案也會進行相同的特殊處理。
Terraform 一開始載入程式碼檔案時會跳過這些重載文件,然後才會依照字典順序一個一個處理重載文件。對重載檔案中定義的所有頂級區塊(resource、data 等),Terraform 會嘗試尋找對應的已有物件並且將重載內容合併進 已有物件。
重載檔案只應使用於特殊場景,過度使用會使得讀者在閱讀原始程式碼檔案時被迫還要閱讀所有的重載檔案才能理解物件配置,從而降低了程式碼的可讀性。使用重載文件時,請在原始文件被重載的部分中添加相應註釋,提醒未來的讀者哪些部分會被重載文件修改。
如果我們有一個名為 example.tf
的程式碼檔案:
resource "aws_instance" "web" {
instance_type = "t2.micro"
ami = "ami-408c7f28"
}
然後我們建立一個名為 override.tf
的檔案:
resource "aws_instance" "web" {
ami = "foo"
}
Terraform 隨後會合併兩者,實際的配置會是這樣的:
resource "aws_instance" "web" {
instance_type = "t2.micro"
ami = "foo"
}
合併行為
不同的區塊類型有著微不同的合併行為,某些特定區塊內的特殊構造會以特殊形式被合併。
一般來說:
- 重載檔案內的頂級區塊會和普通檔案內同類型同名的頂級區塊合併
- 重載檔案內的頂級區塊設定冊參數會覆寫普通檔案內對應區塊內的同名參數
- 重載區塊內的內嵌區塊會取代普通檔案內對應區塊內的所有同類型內嵌區塊。所有重載區塊內沒有定義的內嵌區塊在普通檔案內保持不變
- 內嵌塊的內容不會進行合併
- 合併後的區塊仍然需要符合對應區塊類型的所有驗證規則
如果有多個重載檔案定義了同一個頂級區塊,那麼重載效果是疊加的,後載入的重載區塊會在先前載入的重載區塊生效的基礎上合併。重載操作首先依照檔案名稱的字典序其次是在重載檔案中的位置決定執行順序。
有一些針對特定頂級區塊類型的特殊合併行為規則,我們將重載檔案中定義的區塊稱為重載區塊,重載區塊在普通檔案中對應的區塊稱為來源區塊:
合併 resource 區塊以及合併 data 區塊
在 resource
區塊內,所有 lifecycle
區塊的內容會依照參數逐條合併。比如說,一個重載塊只定義了 create_before_destroy
參數而源塊定義了 ignore_changes
,那麼 create_before_destroy
被合併的同時 igonore_changes
將會被保留。
如果重載的 resource
區塊包含了一個或多個 provisioner
,那麼來源區塊內所有的 provisioner
會被忽略。
如果重載的 resource
區塊內包含了一個 connection
區塊,那麼它將完全覆蓋所有來源區塊內定義的 connection
區塊
不允許在重載區塊內定義 depends_on
參數,那將會引發一個錯誤。
合併 variable 區塊
variable
區塊內參數的合併遵循上述的標準流程,但對於 type
和 default
參數的處理會有一些特殊的考慮。
如果來源區塊定義了 default
值而重載區塊修改了變數的 type
,Terraform 會嘗試將 default
值轉換成新類型,如果轉換失敗則會報錯。
同樣的,如果來源區塊定義了 type
參數而重載區塊修改了 default
值,那麼新的 default
值必須能夠轉換成原先的型別。
合併 output 區塊
不允許在重載區塊內定義 depends_on
參數,這會引發一個錯誤。
合併 locals 區塊
所有的 locals
區塊都定義了一個或多個命名值。針對 locals
的合併會是依照命名值的名字逐條執行的,不論命名值是在哪個 locals
區塊內被定義的。
合併 terraform 區塊
如果重載區塊定義了 required_providers
參數,那麼它的值會被逐條合併,這就允許重載區塊在不影響其他 Provider 的情況下調整單一 Provider 的版本約束。
重載區塊內的 requeired_version
和 required_providers
里的配置完全覆蓋來源區塊內的相應配置。如果來源區塊和重載區塊都定義了 required_version
,那麼來源區塊的配置就會被完全忽略。
程式碼風格規範
Terraform 推薦以下程式碼規格:
- 使用兩個空格縮排
- 同一縮排層級的多個賦值語句以等號對齊:
ami = "abc123"
instance_type = "t2.micro"
- 當塊體內同時有參數賦值以及內嵌塊時,請先寫參數賦值,然後是內嵌塊。參數與內嵌塊之間空一行分隔
- 同時包含參數賦值以及元參數賦值的區塊,請先編寫元參數賦值語句,接著是參數賦值語句,之間空一行分隔。元參數區塊請置於區塊體的最後,空一行分隔
terraform example
resource "aws_instance" "example" {
count = 2 # meta-argument first
ami = "abc123"
instance_type = "t2.micro"
network_interface {
# ...
}
lifecycle { # meta-argument block last
create_before_destroy = true
}
}
- 頂層區塊之間應空一行分隔。內嵌塊之間也應該空一行分隔,除非是相同類型的內嵌塊(例如 resource 塊內部多個 provisioner 塊)
- 同類型區塊之間盡量避免插入其他類型區塊,除非不同類型區塊共同組成了一個有語義的家族(比方說,
aws_instnace
資源內的root_block_device
、ebs_block_device
、ephemeral_block_device
內嵌區塊共同構成了描述 AWS 區塊儲存的區塊家族,所以他們可以被混合編寫)。
Checks
過去我們可以在 resource
區塊裡的 lifecycle
區塊中驗證基礎設施的狀態。check
區塊填補了在 terraform apply
後驗證基礎設施狀態這項功能中的一塊空白。
check
區塊允許我們定義在每次 plan
以及 apply
操作後執行的自訂的驗證。check
區塊定義的驗證邏輯是作為 plan
和 apply
操作的最後一步執行的。
語法
你可以定義一個包含本地名稱的 check
區塊,其中可以定義一個有限作用範圍的 data
區塊,以及至少一個的斷言。
下面的範例示範了載入 Terraform 官網並驗證 HTTP 回傳狀態碼為 200
check
check "health_check" {
data "http" "terraform_io" {
url = "https://www.terraform.io"
}
assert {
condition = data.http.terraform_io.status_code == 200
error_message = "${data.http.terraform_io.url} returned an unhealthy status code"
}
}
有限作用範圍的資料來源
我們可以在 check
區塊使用任意 Provider 提供的任意資料來源作為一個有限作用範圍的資料來源。
一個 check
區塊可以配一個可選的內嵌(也叫有限作用範圍)資料來源。該 data
區塊和普通的 data
區塊行為類似,但你不能在定義它的 check
區塊以外引用它。另外,如果一個有限作用範圍的資料來源運行時觸發了任意錯誤,這些錯誤將被標記為警告,不會阻止 Terraform 繼續執行操作。
你可以使用有限作用範圍的資料來源在 resource
的 lifecycle
外驗證相關基礎設施片段的狀態。在上面的例子裡,如果 terraform_io
資料來源在載入時發生錯誤,那麼我們將會收到一個警告而不是中斷執行的錯誤。
元參數
有限作用域的資料來源支援 depends_on
和 provider
元參數,但不支援 count
或 for_each
元參數。
depends_on
元參數配合有限作用域資料來源可以提供非常強大的能力。
假設上述範例中的 Terraform 網站是我們即將用同一目錄下的 Terraform 程式碼部署的,在第一次創建 Plan 時因為網站還沒有被創建,所以驗證會失敗,Terraform 總是會在一開始顯示一條讓人分心的警告訊息。
我們可以為該內嵌資料來源新增 depends_on
來確保該資料來源依賴某項組成基礎架構的必要資源,例如負載平衡器。這樣對該資料來源的檢查結果將保持 known after apply
直到依賴項建立完成。此策略避免了在配置階段產生無意義的警告訊息,直到在 plan
和 apply
操作的合適階段執行檢查。
該策略的一個問題是如果有限作用域資料來源所依賴的資源發生了變化,那麼 check
區塊將返回 known after apply
直到 Terraform 完成了對被依賴資源的更新。在某些情況下,這種行為將會引發一些問題。
我們推薦只有在內嵌資料來源依賴某項資源,但又沒有明確的引用其資料時使用 depends_on
元參數。
assert 斷言
我們在 check
區塊中使用 assert
區塊定義自訂的斷言條件。每個 check
區塊必須聲明至少一個或更多的 assert
區塊。每個 assert
區塊都包含了一個 condition
屬性與一個 error_message
屬性。
與其他自訂檢查(variable
中的 validation
以及 lifecycle
中的 precondition
和 postcondition
)不同,assert
的斷言不會影響 Terraform 執行操作。失敗的斷言將以警告訊息的形式輸出而不會中斷後續的操作。這與其他諸如 postcondition
這樣的自訂檢查形成了對比,因為它們的檢查失敗會立即終止後續的 plan
以及 apply
操作,傳回錯誤訊息。
assert
區塊中的斷言條件表達式可以引用同一 check
區塊裡的內嵌資料來源數據,以及同一模組中的任意輸入參數、資源、資料來源、模組的輸出值。
check 區塊的元參數
check
塊目前不支援元參數。Terraform 團隊目前正在收集有關此功能的回饋。
是使用 check 區塊還是其他自訂條件檢查
check
區塊提供了 Terraform 中最靈活的驗證功能。我們可以在其中引用輸出值、輸入參數、資源以及資料來源的值。我們的確可以使用 check
區塊取代所有其他的自訂條件檢查,但這並不意味著我們應該這麼做。
check
與其他檢查最大的差別在於 check
區塊不會中斷 Terraform 的執行。我們需要將這種非阻塞性的行為特徵計入考量來決定採取何種檢查。
輸出值與輸入參數
輸出值的 precondition
以及輸入變數的 validation
都可以對輸入輸出值進行斷言。
這些檢查是用來阻止 Terraform 在資料有問題時繼續執行的。
舉例來說,如果輸入參數的值是無效的那麼任由 Terraform 執行整個配置文件並沒有什麼意義,這種情況下,check
塊只會輸出有關無效輸入參數的警告,不會打斷 Terraform 的執行,而 validation
區塊則會警告輸入參數值非法,並終止 Terraform 執行 plan
或 apply
操作。
resource 區塊的 precondition 與 postcondition
check
與 precondition
和 postcondition
的差異更加微妙。
precondition
是自訂條件檢查中最特殊的,因為它們是在資源的變更被計算或應用之前執行的檢查。決定使用 precondition
還是 postcondition
的考量也適用於選擇要使用 precondition
還是 check
區塊。
我們可以在 postcondition
與 check
區塊之間互換來驗證資源和資料來源。例如,我們可以把上述範例中的 check
區塊改寫成 postcondition
,以下的 postcondition
區塊將會驗證對 Terraform 網站的請求是否回傳了狀態碼 200
:
data "http" "terraform_io" {
url = "https://www.terraform.io"
lifecycle {
postcondition {
condition = self.status_code == 200
error_message = "${self.url} returned an unhealthy status code"
}
}
}
check
和 postcondition
區塊都在 plan
或 apply
操作中驗證了 Terraform 網站是否回傳 200
狀態碼,它們的差異是發生錯誤時的行為。
如果是 postcondition
失敗,那麼就無法繼續執行。Terraform 會阻止任意後續的 plan
或 apply
操作。
我們建議使用 check
區塊來驗證基礎設施的整體狀態,僅在希望確保單一資源狀態符合預期時才使用 postcondition
。